package job

import (
	"bytes"
	"cloudops/src/common"
	"cloudops/src/config"
	"context"
	"encoding/json"
	"fmt"
	"go.uber.org/zap"
	"os"
	"os/exec"
	"strings"
	"sync"
	"syscall"
	"time"
)

type Task struct {
	sync.Mutex                // 加锁的目的是 可能有多个人处理，或者多个情况同时发生
	Id                 int    `json:"id"`                 // 任务id
	ScriptType         string `json:"scriptType"`         // 脚本类型 shell python
	Account            string `json:"account"`            // 执行账号
	ScriptContent      string `json:"-"`                  // 脚本内容 来自与模板 从server端获取的
	Args               string `json:"args"`               //	参数 先假定以空格分割
	Status             string `json:"status"`             // 状态
	ExecTimeoutSeconds int    `json:"execTimeoutSeconds"` // 执行超时时间
	ScriptPath         string `json:"scriptPath"`         // 脚本路径
	// 需求是 多个任务的结果 也保存到本地，目的是进行问题排查
	TaskDir        string              `json:"taskDir"` // 任务元信息目录
	Sc             *config.AgentConfig `json:"-"`
	Cmd            *exec.Cmd           `json:"-"`
	Stdout, Stderr bytes.Buffer        `json:"-"` // 标准输出 标准错误
}

// 设置任务目录
func (t *Task) SetTaskDir() {
	t.TaskDir = fmt.Sprintf("%s/%d", t.Sc.JobExecC.MetaDir, t.Id)
	if t.ScriptType == "" {
		t.ScriptType = common.AGENT_TASK_SCRIPT_TYPE_SHELL
	}
}

// 设置状态
func (t *Task) SetStatus(status string) {
	t.Lock()
	defer t.Unlock()
	t.Status = status
}

// 获取标准输出
func (t *Task) GetStdout() string {
	t.Lock()
	defer t.Unlock()
	return t.Stdout.String()
}

// 获取标准错误
func (t *Task) GetStderr() string {
	t.Lock()
	defer t.Unlock()
	return t.Stderr.String()
}

// 获取状态
func (t *Task) GetStatus() string {
	t.Lock()
	defer t.Unlock()
	return t.Status
}

// 任务开始前的准备工作
func (t *Task) Prepare() (err error) {
	err = os.MkdirAll(t.TaskDir, os.ModePerm)
	if err != nil {
		t.Sc.Logger.Error("任务开始前创建元信息目录失败",
			zap.Error(err),
			zap.Any("任务id", t.Id),
		)
		return
	}

	// 0755的目的是 还会有其他用户可能执行
	scriptPath := fmt.Sprintf("%s/script", t.TaskDir)
	t.ScriptPath = scriptPath
	err = os.WriteFile(scriptPath, []byte(t.ScriptContent), 0755)
	if err != nil {
		t.Sc.Logger.Error("任务开始前写入脚本失败",
			zap.Error(err),
			zap.Any("任务id", t.Id),
		)
		return
	}

	// 把元信息写入
	metaJson, err := json.Marshal(t)
	if err != nil {
		t.Sc.Logger.Error("任务开始前写入元信息文件json失败",
			zap.Error(err),
			zap.Any("任务id", t.Id),
		)
		return
	}

	err = os.WriteFile(fmt.Sprintf("%s/meta.json", t.TaskDir), metaJson, 0644)
	if err != nil {
		t.Sc.Logger.Error("任务开始前写入元信息文件json失败",
			zap.Error(err),
			zap.Any("任务id", t.Id),
		)
		return
	}

	startTime := time.Now().Format("2006-01-02 15:04:05")
	err = os.WriteFile(fmt.Sprintf("%s/startTime", t.TaskDir), []byte(startTime), 0644)
	if err != nil {
		t.Sc.Logger.Error("任务开始前写入启动时间文件失败",
			zap.Error(err),
			zap.Any("任务id", t.Id),
		)
		return
	}
	return nil
}

// 紧急kill的方法
func (t *Task) Kill() {
	if t.Cmd == nil {
		return
	}
	t.Sc.Logger.Info("任务准备kill了",
		zap.Any("任务id", t.Id),
		zap.Any("进程id", t.Cmd.Process.Pid),
	)
	err := syscall.Kill(-t.Cmd.Process.Pid, syscall.SIGKILL)
	if err != nil {
		t.Sc.Logger.Error("任务准备kill失败",
			zap.Error(err),
			zap.Any("任务id", t.Id),
		)
	}
}

// 执行逻辑 直接context版本
func (t *Task) Start() error {
	// 在start之前 先 执行prepare
	err := t.Prepare()
	if err != nil {
		t.Sc.Logger.Error("任务准备失败",
			zap.Error(err),
			zap.Any("任务id", t.Id),
		)
		return err
	}

	t.Sc.Logger.Info("任务开始前的准备工作已完成",
		zap.Any("任务id", t.Id),
	)
	if t.ExecTimeoutSeconds == 0 {
		t.ExecTimeoutSeconds = t.Sc.JobExecC.ExecTimeoutSeconds
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(t.ExecTimeoutSeconds)*time.Second)
	// 这里的cancel 会在这个函数返回的时候直接执行
	// 所以 下面的结果处理 不能异步做，因为会导致 无法开始执行就被kill
	defer cancel()
	// 强制它 bash -c ，规避  kill 进程组用法 引入的syscall  在window 上的编译问题
	// new cmd 对象
	// 判断用户
	cmdName := ""
	cmdArgs := []string{}

	// 为了解决 bash -c 传参问题
	// 需要将 参数和脚本放一起
	scriptWithArgs := fmt.Sprintf("%s %s", t.ScriptPath, t.Args)

	// 先不考虑python问题 只考虑 account
	if t.Account == "root" {
		// sh 或者bash
		//cmdName = "sh"
		cmdName = t.Sc.JobExecC.BashBinPath
		cmdArgs = []string{
			"-c",
			scriptWithArgs,
		}
	} else {
		cmdName = "su"
		cmdArgs = []string{
			t.Account,
			"-c",
			scriptWithArgs,
		}
	}

	cmd := exec.CommandContext(ctx, cmdName, cmdArgs...)
	t.Sc.Logger.Info("任务的命令打印",
		zap.Any("任务id", t.Id),
		zap.Any("任务脚本类型", t.ScriptType),
		zap.Any("命令cmd-Args", cmd.Args),
		zap.Any("命令cmd-Env", cmd.Env),
		zap.Any("命令cmd-Dir", cmd.Dir),
		zap.Any("命令cmd-Path", cmd.Path),
	)
	cmd.Stdout = &t.Stdout
	cmd.Stderr = &t.Stderr

	// 后面我们可能会启用 kill 进程组用法
	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
	go func() {
		select {
		case <-ctx.Done():
			// cmd.Process.Kill()
			syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
		}
	}()

	t.Cmd = cmd
	// 这里不使用 CombinedOutput的目的是  CombinedOutput里面又会去设置 stdout stderr
	t.SetStatus(common.AGENT_TASK_STATUS_RUNNING)
	err = cmd.Start()
	if err != nil {
		t.Sc.Logger.Error("任务Start失败",
			zap.Error(err),
			zap.Any("任务id", t.Id),
		)
		return err
	}
	// 同步处理结果
	t.runProcess()
	return nil
}

// 设置状态，等待结果，写入文件
func (t *Task) runProcess() {
	err := t.Cmd.Wait()
	if err != nil {
		if strings.Contains(err.Error(), "signal: killed") {
			t.SetStatus(common.AGENT_TASK_STATUS_KILLED)
			t.Sc.Logger.Error("任务被人为或触发超时kill",
				zap.Error(err),
				zap.Any("任务id", t.Id),
			)
		} else {
			t.SetStatus(common.AGENT_TASK_STATUS_FAILED)
			t.Sc.Logger.Error("任务执行出错",
				zap.Error(err),
				zap.Any("任务id", t.Id),
			)
		}
	} else {
		// 执行成功
		t.SetStatus(common.AGENT_TASK_STATUS_SUCCESS)
		t.Sc.Logger.Info("任务执行成功",
			zap.Error(err),
			zap.Any("任务id", t.Id),
		)
	}
	// 处理任务的结果
	t.persistResult()
}

// 任务结果落盘 ，保存到文件
// stdout stderr 分别读取出来 写入对应的文件
func (t *Task) persistResult() {
	stdOutPath := fmt.Sprintf("%s/stdout", t.TaskDir)
	stdErrPath := fmt.Sprintf("%s/stderr", t.TaskDir)
	statusPath := fmt.Sprintf("%s/status", t.TaskDir)

	// 写标准输出
	err := common.WriteFileWithString(stdOutPath, t.GetStdout())
	if err != nil {
		t.Sc.Logger.Error("任务执行完结果写入文件stdout失败",
			zap.Error(err),
			zap.Any("任务id", t.Id),
			zap.Any("标准输出", stdOutPath),
		)
	}

	// 标准错误
	err = common.WriteFileWithString(stdErrPath, t.GetStderr())
	if err != nil {
		t.Sc.Logger.Error("任务执行完结果写入文件失败",
			zap.Error(err),
			zap.Any("任务id", t.Id),
			zap.Any("标准错误", stdErrPath),
		)
	}

	// 写最终的状态
	err = common.WriteFileWithString(statusPath, t.GetStatus())
	if err != nil {
		t.Sc.Logger.Error("任务执行完结果写入文件失败",
			zap.Error(err),
			zap.Any("任务id", t.Id),
			zap.Any("状态结果", statusPath),
		)
	}
	return
}
